﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text.RegularExpressions;
using System.IO;
using System.Reflection.Emit;
using System.Linq;

namespace BeanboxSoftware.BeanProxy.Internals
{
  public abstract class BuilderBase
  {
    public List<AssemblyName> ReferencedAssemblies { get; set; }
    public static Dictionary<Type, TypeLoader> Assemblies { get; set; }
    public List<Type> ReferencedTypes { get; set; }

    public static string MsCorLib { get { return "mscorlib"; } }
    public static string MethodDefinitionsPropertyName { get { return "MethodDefinitions"; } }
    public static string SafeNamePrefix { get { return "____beanbox_software_proxy_variable_name_prefix_"; } }
    public static string GetOutParameter { get { return "GetOutParameter"; } }
    public static string This { get { return "this"; } }
    public static string BuilderName { get { return "Builder"; } }
    public static string RaiseEventPrefix { get { return "________EventToRaise"; } }
    public static BindingFlags Flags { get { return BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; } }
    public static string Space { get { return " "; } }
    public static string New { get { return "new"; } }
    public static string Public { get { return "public"; } }
    public static string Protected { get { return "protected"; } }
    public static string Private { get { return "private"; } }
    public static string Class { get { return "class"; } }
    public static string Get { get { return "get"; } }
    public static string Set { get { return "set"; } }
    public static string Semicolon { get { return ";"; } }
    public static string LocalMethodConfig { get { return "_____proxy___method___config"; } }
    public static string LocalWrapperVariable { get { return "___wrapper"; } }
    public static string Equal { get { return "="; } }
    public static string Comma { get { return ","; } }
    public static string If { get { return "if"; } }
    public static string Dot { get { return "."; } }
    public static string IsReal { get { return "IsReal"; } }
    public static string Base { get { return "base"; } }
    public static string Count { get { return "Count"; } }
    public static string Increment { get { return "++"; } }
    public static string Return { get { return "return"; } }
    public static string Namespace { get { return "namespace"; } }
    public static string Colon { get { return ":"; } }
    public static string NewLine { get { return "\r\n"; } }
    public static string Value { get { return "value"; } }
    public static string Override { get { return "override"; } }
    public static string Virtual { get { return "virtual"; } }
    public static string VoidName { get { return "void"; } }
    public static string Intercept { get { return "Intercept"; } }
    public static string GetPrefix { get { return "get_"; } }
    public static string SetPrefix { get { return "set_"; } }
    public static string Ref { get { return "ref"; } }
    public static string Out { get { return "out"; } }
    public static string Throw { get { return "throw"; } }
    public static string AbtractMessageFormat { get { return "{0} is abstract in {1} and therefore must not be set to real."; } }
    public static string Event { get { return "event"; } }
    public static string ConfiguredProperty { get { return "MethodConfigurationsReady"; } }
    public static string Bang { get { return "!"; } }
    public static string ExpectingPropertyUseName { get { return "ExpectingPropertyUse"; } }
    public static string IProxyName { get { return string.Format("{0}.{1}", typeof(IProxy).Namespace, "IProxy"); } }
    public static string Var { get { return "var"; } }
    public static string Typeof { get { return "typeof"; } }
    public static string GetField { get { return "GetField"; } }
    public static string GetValue { get { return "GetValue"; } }
    public static string Invoke { get { return "Invoke"; } }
    public static string FieldType { get { return "FieldType"; } }
    public static string GetProperty { get { return "GetProperty"; } }
    public static string Null { get { return "null"; } }
    public static string Pipe { get { return "|"; } }

    static BuilderBase()
    {
      Assemblies = new Dictionary<Type, TypeLoader>();
    }

    public BuilderBase()
    {
      ReferencedAssemblies = new List<AssemblyName>();
      ReferencedTypes = new List<Type>();

      TryAddReference(Assembly.GetCallingAssembly());
      TryAddReference(Assembly.GetEntryAssembly());
      TryAddReference(Assembly.GetExecutingAssembly());
      TryAddReference(Assembly.GetAssembly(typeof(Action)));
    }

    /// <summary>
    /// Add a list of AssemblyName's to the static cache.
    /// </summary>
    /// <param name="names">List to add</param>
    public void TryAddReferences(IList<AssemblyName> names)
    {
      foreach (var name in names)
      {
        TryAddReference(name);
      }
    }

    /// <summary>
    /// Add a single AssemblyName to the static cache.
    /// </summary>
    /// <param name="name">AssemblyName to add</param>
    public void TryAddReference(AssemblyName name)
    {
      if (
        name.Name == MsCorLib || 
        ReferencedAssemblies.FirstOrDefault(asm => asm.FullName == name.FullName) != null)
      {
        return;
      }

      ReferencedAssemblies.Add(name);
    }

    /// <summary>
    /// Adds the assembly from which type comes, as well as the assemblies inherited by type
    /// </summary>
    /// <param name="type">type used to lookup needed assemblies</param>
    public void TryAddReference(Type type)
    {
      if (type == null)
      {
        return;
      }

      if (ReferencedTypes.Contains(type))
      {
        return;
      }

      ReferencedTypes.Add(type);

      TryAddReference(type.Assembly);

      foreach (Type inherited in type.GetInterfaces())
      {
        TryAddReference(inherited);
      }

      TryAddReference(type.BaseType);
    }

    /// <summary>
    /// Add a single AssemblyName to the static cache (assembly.GetName() is used).
    /// </summary>
    /// <param name="assembly">assembly to add</param>
    public void TryAddReference(Assembly assembly)
    {
      if (assembly == null)
      {
        return;
      }

      TryAddReference(assembly.GetName());
    }

    /// <summary>
    /// Creates a default value (null for reference types)
    /// </summary>
    /// <param name="t">target type</param>
    /// <returns>default value</returns>
    public static object Default(Type t)
    {
      if (t.IsValueType)
      {
        return Activator.CreateInstance(t, false);
      }
      else
      {
        return null;
      }
    }
    /// <summary>
    /// Invokes Default(typeof(T));
    /// </summary>
    /// <typeparam name="T">target type</typeparam>
    /// <returns>default value</returns>
    public static T Default<T>()
    {
      return (T)Default(typeof(T));
    }

    public abstract IEnumerable<MethodInfo> AllMethods(bool includeProps);
    public abstract IEnumerable<EventInfo> AllEvents();
  }

  public class Builder<T> : BuilderBase
  {
    public List<MethodDefinition> MethodDefinitions { get; protected set; }

    public CtorConfig CtorConfig { get; set; }

    public Builder()
      : this(new CtorConfig())
    {
    }

    public Builder(CtorConfig config)
    {
      CtorConfig = config;
      MethodDefinitions = new List<MethodDefinition>();
    }

    public Type ProxyType { get { return typeof(T); } }
    public string ProxyNamespace { get { return string.Format("{0}", ProxyType.Namespace); } }
    public string ClassName
    {
      get
      {
        string name = ProxyType.Name;

        if (ProxyType.IsGenericType)
        {
          name = name.Substring(0, name.Length - 2);
        }

        return string.Format("Proxy{0}", name);
      }
    }

    public virtual Assembly GetAssembly()
    {
      lock (Assemblies)
      {
        if (!Assemblies.ContainsKey(ProxyType))
        {
          var loader = new TypeLoader();
          loader.Type = ProxyType;
          loader.Assembly = BuildAssembly();
          loader.MethodDefinitions = MethodDefinitions;

          Assemblies[ProxyType] = loader;
        }
        else
        {
          MethodDefinitions = Assemblies[ProxyType].MethodDefinitions;
        }

        return Assemblies[ProxyType].Assembly;
      }
    }

    public virtual Assembly BuildAssembly()
    {
      if (ProxyType.IsNotPublic || ProxyType.IsSealed)
      {
        throw new ArgumentException(GetType().Name + " cannot build nonpublic or sealed classes");
      }

      if (!ProxyType.IsInterface && GetConstructors().Count == 0)
      {
        throw new ArgumentException(string.Format(
          "It appears {0} only has internal constructors and therefore a proxy cannot be made.",
          ProxyType));
      }

      var provider = CreateCompiler();
      PrepareMethodDefinitions();
      string entireSource = BuildProxySourceCode();

      CompilerParameters options = BuildProviderOptions();
      var results = provider.CompileAssemblyFromSource(options, entireSource);

      string errorString = "";

      foreach (CompilerError error in results.Errors)
      {
        errorString += error.ToString() + "\r\n";
      }

      if (results.Errors.Count > 0)
      {
        throw new ArgumentException(string.Format("Failed to compile Proxy for {0}. {1}\r\n{2}", typeof(T).Name, errorString, entireSource));
      }

      var assembly = results.CompiledAssembly;
      return assembly;
    }

    public virtual void PrepareMethodDefinitions()
    {
      foreach (MethodInfo mi in AllMethods(true))
      {
        var md = new MethodDefinition(mi);
        MethodDefinitions.Add(md);
      }
    }

    public virtual CSharpCodeProvider CreateCompiler()
    {
      var options = new Dictionary<string, string>();
      options.Add("CompilerVersion", "v4.0");
      var cscp = new CSharpCodeProvider(options);

      return cscp;
    }

    public virtual CompilerParameters BuildProviderOptions()
    {
      var options = new CompilerParameters()
      {
        CompilerOptions = "",
        TreatWarningsAsErrors = false,
        IncludeDebugInformation = true,
        GenerateInMemory = true,
        GenerateExecutable = false
      };

      TryAddReferences(ProxyType.Assembly.GetReferencedAssemblies());
      TryAddReference(Assembly.GetAssembly(ProxyType));

      foreach (var genericType in ProxyType.GetGenericArguments())
      {
        TryAddReference(Assembly.GetAssembly(genericType));
      }

      foreach (var assemblyName in ReferencedAssemblies)
      {
        var asm = Assembly.Load(assemblyName.FullName);
        options.ReferencedAssemblies.Add(asm.Location);
      }

      return options;
    }

    public T CreateProxy()
    {
      var assembly = GetAssembly();

      object instance = CreateInstance(assembly);

      ConfigureMethods((IProxy)instance);

      return (T)instance;
    }

    public virtual object CreateInstance(Assembly assembly)
    {
      string fullname = string.Format("{0}.{1}", ProxyNamespace, ClassName);

      var ctor = CtorConfig.GetCtor(assembly.GetType(fullname, true, false));
      var instance = ctor.Invoke(CtorConfig.GetValues());

      return instance;
    }

    public virtual string BuildProxySourceCode()
    {
      string classSource = "";

      classSource += Namespace + Space + ProxyNamespace;

      var namespaceBody = new Scope(Scope.WrapTypeEnum.Brackets);
      var classBody = new Scope(Scope.WrapTypeEnum.Brackets);

      namespaceBody.Add("");
      namespaceBody.Add(Public + Space + Class + Space + ClassName + Space +
          Colon + Space +
          BuildTypeName(ProxyType) +
          Comma + Space +
          IProxyName);

      classBody.Add(BuildProperties());
      classBody.Add(BuildMethods());
      classBody.Add(BuildEvents());
      classBody.Add(BuildConstructors());
      classBody.Add(BuildIProxy()); //must be last for Reset()

      namespaceBody.Add(classBody.ToString());
      classSource += namespaceBody.ToString();

      return classSource;
    }

    public virtual string BuildProperties()
    {
      string propertySource = "";

      foreach (var pi in AllProperties())
      {
        propertySource += BuildProperty(pi);
      }

      return propertySource;
    }

    public virtual string BuildMethods()
    {
      string source = "";
      foreach (var method in AllMethods(false))
      {
        source += BuildMethod(method);
      }

      return source;
    }

    public virtual string BuildEvents()
    {
      string eventSource = "";

      foreach (var ev in AllEvents())
      {
        eventSource += BuildEvent(ev);
      }

      return eventSource;
    }

    public virtual IEnumerable<PropertyInfo> AllProperties()
    {
      var set = new List<string>();

      foreach (var pi in ProxyType.GetProperties(Flags))
      {
        var piName = pi.Name;
        set.Add(piName);

        if (!Valid(pi))
        {
          continue;
        }

        yield return pi;
      }

      foreach (var inf in ProxyType.GetInterfaces())
      {
        foreach (var pi in inf.GetProperties(Flags))
        {
          if (!Valid(pi))
          {
            continue;
          }

          var piName = pi.Name;
          if (set.Contains(piName))
          {
            continue;
          }

          set.Add(piName);
          yield return pi;
        }
      }
    }

    public override IEnumerable<MethodInfo> AllMethods(bool includeProps)
    {
      foreach (var mi in AllMethods(ProxyType, includeProps))
        yield return mi;
    }

    public virtual IEnumerable<MethodInfo> AllMethods(Type targetType, bool includeProps)
    {
      var set = new List<MethodDefinition>();

      foreach (var mi in targetType.GetMethods(Flags))
      {
        var def = new MethodDefinition(mi);
        set.Add(def);

        if (!Valid(mi, includeProps)) continue;
        yield return mi;
      }

      foreach (var inf in targetType.GetInterfaces())
      {
        foreach (var mi in inf.GetMethods(Flags))
        {
          if (!Valid(mi, includeProps)) continue;

          var def = set.Find(mi);

          if (def != null) continue;

          def = new MethodDefinition(mi);
          set.Add(def);

          yield return mi;
        }
      }
    }

    public override IEnumerable<EventInfo> AllEvents()
    {
      var set = new List<string>();

      foreach (var ei in ProxyType.GetEvents(Flags))
      {
        var miName = ei.Name;
        set.Add(miName);
        yield return ei;
      }

      foreach (var inf in ProxyType.GetInterfaces())
      {
        foreach (var ei in inf.GetEvents(Flags))
        {
          var miName = ei.Name;

          if (set.Contains(miName))
          {
            continue;
          }

          set.Add(miName);
          yield return ei;
        }
      }
    }

    public virtual string BuildEvent(EventInfo ev)
    {
      string source = "";

      bool needsDef = EventNeedsDefinition(ev);

      source += BuildEventProperty(ev, needsDef);
      source += BuildEventInvoker(ev, needsDef);

      return source;
    }

    public virtual bool EventNeedsDefinition(EventInfo ev)
    {
      if (ev.DeclaringType.IsClass && !ev.DeclaringType.IsAbstract)
      {
        return false;
      }

      if (!GetAddMethod(ev).IsAbstract)
      {
        return false;
      }

      return true;
    }

    public virtual MethodInfo GetAddMethod(EventInfo ev)
    {
      return ev.GetAddMethod() ?? ev.GetAddMethod(true);
    }
    public virtual MethodInfo GetRemoveMethod(EventInfo ev)
    {
      return ev.GetRemoveMethod() ?? ev.GetRemoveMethod(true);
    }

    public virtual string BuildEventProperty(EventInfo ev, bool needsDef)
    {
      if (!needsDef)
      {
        return "";
      }

      var eventType = ev.EventHandlerType;
      MethodInfo eventMethodInfo = GetAddMethod(ev);

      var eventTypeName = BuildTypeName(eventType);

      string source = "";
      source += BuildAccess(eventMethodInfo);
      source += Space + BuildOverride(eventMethodInfo) + Space;
      source += Event + Space;
      source += eventTypeName + Space;
      source += ev.Name + Semicolon + NewLine;
      return source;
    }

    public virtual string BuildEventInvoker(EventInfo ev, bool needsDef)
    {
      Type eventType = ev.EventHandlerType;
      MethodInfo eventMethodInfo = eventType.GetMethod(Invoke, Flags);

      string source = "";

      source += BuildMethodSignature(eventMethodInfo, "", RaiseEventPrefix + ev.Name);

      var body = new Scope(Scope.WrapTypeEnum.Brackets);
      var returnType = eventMethodInfo.ReturnType;
      string returnCast = IsVoid(returnType) ? "" : (Return + Space + Scope.Parenthesis(BuildTypeName(returnType)));

      //if the parent gives this event as abstract or if it is an interfaced event - call your own
      if (needsDef)
      {
        string call = Scope.Parenthesis(BuildParams(false, true, eventMethodInfo.GetParameters()));
        body.Add(returnCast + ev.Name + call + Semicolon);
      }
      //else, use reflection to get the event fired
      else
      {
        string flagType = BuildTypeName(typeof(BindingFlags));

        Func<BindingFlags, BindingFlags, string> getFlags = (f1, f2) =>
        {
          string flagString = "";
          flagString += flagType + Dot + f1.ToString();
          flagString += "|";
          flagString += flagType + Dot + f2.ToString();

          return flagString;
        };

        Func<BindingFlags, string> getFlagName = (flag) =>
        {
          string flagString = "";
          flagString += flagType + Dot + flag.ToString();
          return flagString;
        };

        string type = "typ";
        string inst = "instance";
        string raiseRef = "raiseRef";
        string raiseMethod = "raiseMethod";

        string evt = "event_info";
        string methodField = "Method";
        string targetField = "Target";

        body.Add(Var + Space + type + Equal + Typeof + Scope.Parenthesis(BuildTypeName(ev.DeclaringType)) + Semicolon);
        body.Add(Var + Space + evt + Equal + type + Dot + GetField +
          Scope.Parenthesis
          (
            Scope.Quote(ev.Name),
            getFlagName(BindingFlags.NonPublic) +
            Pipe +
            getFlagName(BindingFlags.Instance)
          ) +
          Semicolon
        );

        body.Add(Var + Space + methodField + Equal + evt + Dot + FieldType + Dot + GetProperty +
          Scope.Parenthesis
          (
            Scope.Quote(methodField),
            getFlagName(BindingFlags.Public) + Pipe + getFlagName(BindingFlags.Instance)
          ) +
          Semicolon
        );

        body.Add(Var + Space + targetField + Equal + evt + Dot + FieldType + Dot + GetProperty +
          Scope.Parenthesis
          (
            Scope.Quote(targetField),
            getFlagName(BindingFlags.Public) + Pipe + getFlagName(BindingFlags.Instance)
          ) +
          Semicolon
        );

        body.Add(Var + Space + inst + Equal + evt + Dot + GetValue + Scope.Parenthesis(This) + Semicolon);
        body.Add(Var + Space + raiseRef + Equal + targetField + Dot + GetValue + Scope.Parenthesis(inst, Null) + Semicolon);
        body.Add(Var + Space + raiseMethod + Equal + Scope.Parenthesis(BuildTypeName(typeof(MethodInfo)))
          + methodField + Dot + GetValue + Scope.Parenthesis(inst, Null) + Semicolon);

        body.Add(BuildFakeInvoke(eventMethodInfo, raiseMethod + Dot + Invoke));
      }

      source += body.ToString();

      return source;
    }

    public virtual List<ConstructorInfo> GetConstructors()
    {
      var ctors = new List<ConstructorInfo>();

      foreach (var ctor in ProxyType.GetConstructors(Flags))
      {
        if (!IsInternal(ctor))
        {
          ctors.Add(ctor);
        }
      }

      return ctors;
    }

    public virtual string BuildConstructors()
    {
      string source = "";

      foreach (var ctor in GetConstructors())
      {
        source += BuildConstructor(ctor);
      }

      return source;
    }

    public virtual string BuildConstructor(ConstructorInfo ctor)
    {
      string source = "";

      source += Public + Space + ClassName + Scope.Parenthesis(BuildParams(true, true, ctor.GetParameters()));
      source += NewLine + Colon + Base + Scope.Parenthesis(BuildParams(false, true, ctor.GetParameters()));
      source += Scope.Brackets();

      return source;
    }

    public virtual string BuildIProxy()
    {
      string source = "";

      foreach (var prop in typeof(IProxy).GetProperties(Flags))
      {
        source += BuildIProxyProperty(prop);
      }

      return source;
    }

    public virtual bool NeedsSpecialization(MethodInfo methodInfo)
    {
      var generics = methodInfo.GetGenericArguments();
      foreach (Type genericType in generics)
      {
        if (string.IsNullOrEmpty(genericType.FullName)) //this is called on a templated method, not specialized
        {
          return true;
        }
      }

      return false;
    }

    public virtual string BuildIProxyProperty(PropertyInfo prop)
    {
      string source = "";

      string propName = prop.Name;
      string propField = "_" + propName.Substring(0, 1).ToLower() + propName.Substring(1);

      var propBody = new Scope(Scope.WrapTypeEnum.Brackets);
      var getBody = new Scope(Scope.WrapTypeEnum.Brackets);
      var setBody = new Scope(Scope.WrapTypeEnum.Brackets);

      source += Private + Space + BuildTypeName(prop.PropertyType) + Space + propField + Semicolon;
      source += NewLine;
      source += Public + Space + BuildTypeName(prop.PropertyType) + Space + propName + Space;

      getBody.Add(Return + Space + propField + Semicolon);
      setBody.Add(propField + Space + Equal + Space + Value + Semicolon);

      propBody.Clear();
      propBody.Add(Get + Space + getBody.ToString());
      propBody.Add(Set + Space + setBody.ToString());
      source += propBody.ToString();

      return source;
    }

    public virtual bool Valid(PropertyInfo pi)
    {
      var accessors = new List<MethodInfo>();
      accessors.AddRange(pi.GetAccessors(true));
      accessors.AddRange(pi.GetAccessors(false));

      foreach (var accessor in accessors)
      {
        if (IsVirtual(accessor))
        {
          return true;
        }
      }

      return false;
    }

    public virtual string BuildProperty(PropertyInfo pi)
    {
      string propSource = "";

      string name = pi.Name;
      string access = PropertyIsPublic(pi) ? Public : Protected;

      propSource += access + Space + BuildOverride(pi) + Space + Space + BuildTypeName(pi.PropertyType) + Space + name;

      var propBody = new Scope(Scope.WrapTypeEnum.Brackets);

      propBody.Add(BuildGet(pi));
      propBody.Add(BuildSet(pi));

      propSource += propBody.ToString();

      return propSource;
    }

    /// <summary>
    /// Returns true if pi is an internal property and not visible to the current Assembly
    /// </summary>
    /// <param name="pi">PropertyInfo to check for IsInternal</param>
    /// <returns></returns>
    public virtual bool IsInternal(PropertyInfo pi)
    {
      return IsInternal(pi.GetGetMethod()) || IsInternal(pi.GetSetMethod());
    }

    /// <summary>
    /// Returns true if method is an internal method and not visible to the current Assembly
    /// </summary>
    /// <param name="method">MethodInfo to check for IsInternal</param>
    /// <returns></returns>
    public virtual bool IsInternal(MethodInfo method)
    {
      if (method == null)
      {
        return false;
      }

      if (method.IsAssembly && IsExternal(method.DeclaringType.Assembly))
      {
        return true;
      }

      if (IsInternal(method.ReflectedType))
      {
        return true;
      }

      foreach (var parm in method.GetParameters())
      {
        if (IsInternal(parm.ParameterType))
        {
          return true;
        }
      }

      return false;
    }

    public virtual bool IsInternal(ConstructorInfo method)
    {
      if (method == null)
      {
        return false;
      }

      if (method.IsAssembly && IsExternal(method.DeclaringType.Assembly))
      {
        return true;
      }

      if (IsInternal(method.ReflectedType))
      {
        return true;
      }

      foreach (var parm in method.GetParameters())
      {
        if (IsInternal(parm.ParameterType))
        {
          return true;
        }
      }

      return false;
    }

    /// <summary>
    /// Returns true if type is an internal property and not visible to the current Assembly
    /// </summary>
    /// <param name="type">Type to check for IsInternal</param>
    /// <returns></returns>
    public virtual bool IsInternal(Type type)
    {
      if (!type.IsVisible && IsExternal(type.Assembly))
      {
        return true;
      }

      return false;
    }
    /// <summary>
    /// Returns true if given Assembly is not the current Assembly
    /// </summary>
    /// <param name="asm">Assembly to check against current Assembly</param>
    /// <returns></returns>
    public virtual bool IsExternal(Assembly asm)
    {
      return asm != Assembly.GetExecutingAssembly();
    }

    public virtual bool PropertyIsPublic(PropertyInfo pi)
    {
      var method = GetMostPublicPropertyMethod(pi);

      return method.IsPublic;
    }

    public virtual string BuildGet(PropertyInfo pi)
    {
      var get = GetGetProperty(pi);
      if (get == null || get.IsPrivate)
      {
        return string.Empty;
      }

      string source = "";
      source += BuildAccess(get, GetMostPublicPropertyMethod(pi)) + Get + NewLine;

      var body = new Scope(Scope.WrapTypeEnum.Brackets);
      
      body.Add(GetProxyMethod(get));
      body.Add(BuildExpectingPropertyUse());

      body.Add(BuildCount());

      body.Add(BuildIfReal(get, BuildRealInvoke(true, get)));

      body.Add(BuildFakeInvoke(get));

      source += body.ToString();

      return source;
    }

    public virtual string BuildSet(PropertyInfo pi)
    {
      var set = GetSetProperty(pi);
      if (set == null || set.IsPrivate)
      {
        return string.Empty;
      }

      string source = "";
      source += BuildAccess(set, GetMostPublicPropertyMethod(pi)) + Set + NewLine;

      var body = new Scope(Scope.WrapTypeEnum.Brackets);

      body.Add(GetProxyMethod(set));
      body.Add(BuildExpectingPropertyUse());
      body.Add(BuildCount());
      body.Add(BuildIfReal(set, BuildRealInvoke(true, set)));

      body.Add(BuildFakeInvoke(set));

      source += body.ToString();

      return source;
    }

    public virtual string BuildExpectingPropertyUse()
    {
      string source = "";

      source += If + Space + Scope.Parenthesis(ExpectingPropertyUseName);

      var body = new Scope(Scope.WrapTypeEnum.Brackets);
      body.Add(ExpectingPropertyUseName + Space + Equal + Space + false.ToString().ToLower() + Semicolon);
      body.Add(Throw + Space + New + Space + BuildTypeName(typeof(ExpectingPropertyException)) + Scope.Parenthesis(LocalMethodConfig) + Semicolon);

      source += body.ToString();

      return source;
    }

    public virtual MethodInfo GetMostPublicPropertyMethod(PropertyInfo pi)
    {
      var get = GetGetProperty(pi);
      var set = GetSetProperty(pi);

      if (get == null || get.IsPrivate)
      {
        return set;
      }

      if (set == null || set.IsPrivate)
      {
        return get;
      }

      if (set.IsPublic)
      {
        return set;
      }

      return get;
    }

    public virtual string BuildTypeName(Type type)
    {
      return BuildTypeName(type, type.GetGenericArguments());
    }
    public virtual string BuildTypeName(Type type, Type[] genericParameters)
    {
      TryAddReference(type);


      var elementType = (type.IsArray || type.IsByRef) ? type.GetElementType() : type;
      string name = elementType.Name;

      var gens = genericParameters.ToList();
      var myGens = new Type[] { };
      var parentGens = new Type[] { };

      if (elementType.IsGenericType)
      {
        int backticIndex = name.IndexOf('`');
        int tic = backticIndex >= 0 ? int.Parse(name.Substring(backticIndex + 1)) : 0;
        int count = gens.Count;

        if (backticIndex > 0)
        {
          name = name.Substring(0, backticIndex);
        }

        int mine = tic;
        int hers = count - tic;

        if (mine > 0)
        {
          myGens = gens.GetRange(hers, tic).ToArray();
        }

        if (hers > 0)
        {
          parentGens = gens.GetRange(0, hers).ToArray();
        }
      }

      string namespacePrefix = ""; //empty for template types

      if (!elementType.IsGenericParameter)
      {
        if (elementType.DeclaringType != null)
        {
          //before we move upward, if this type has generic parms - but does not contain generic args, they must belong upward
          namespacePrefix = BuildTypeName(elementType.DeclaringType, parentGens);
        }
        else
        {
          namespacePrefix = "global::" + elementType.Namespace;
        }

        namespacePrefix += Dot;
      }

      if (elementType.IsGenericType)
      {
        //special nullable condition
        if (elementType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
          namespacePrefix = ""; //the namespacePrefix is recreated in this next BuildTypeName
          name = BuildTypeName(myGens[0]) + "?";
        }
        else if (myGens.Length > 0) //how else can i determine if the generics belong to this object?!?
        {
          var template = new Scope(Scope.WrapTypeEnum.Angle);

          template.Add(BuildTypeList(myGens));

          name += template.ToString();
        }
        else 
        {
          //do nothing?
        }
      }

      if (type.IsArray)
      {
        name += Scope.Square();
      }

      name = name.Replace("&", "");

      return string.Format("{0}{1}", namespacePrefix, name);
    }

    public virtual string BuildTypeList(Type[] types)
    {
      string typeString = "";
      bool first = true;

      foreach (var type in types)
      {
        if (!first)
        {
          typeString += Comma + Space;
        }

        first = false;

        typeString += BuildTypeName(type);
      }

      return typeString;
    }

    public virtual MethodInfo GetGetProperty(PropertyInfo pi)
    {
      if (!pi.CanRead)
      {
        return null;
      }

      MethodInfo get = pi.GetGetMethod(false);

      if (get == null)
      {
        get = pi.GetGetMethod(true);
      }

      return get;
    }

    public virtual MethodInfo GetSetProperty(PropertyInfo pi)
    {
      if (!pi.CanWrite)
      {
        return null;
      }

      MethodInfo set = pi.GetSetMethod(false);

      if (set == null)
      {
        set = pi.GetSetMethod(true);
      }

      return set;
    }

    public virtual string GetProxyMethod(MethodInfo method)
    {
      string source = "";

      string guid = "guid";
      var md = MethodDefinitions.Find(method);
      source += Var + Space + guid + Equal + New + Space +
        BuildTypeName(typeof(Guid)) + Scope.Parenthesis(Scope.Quote(md.Id.ToString("D"))) + Semicolon + NewLine;
      source += Var + Space + LocalMethodConfig + Equal +
        BuildTypeName(typeof(MethodDefinition)) + Dot + "Lookup(" + MethodDefinitionsPropertyName + ", guid)" +
        //source += 
        //Var + Space + LocalMethodConfig + Equal + MethodDefinitionsPropertyName +
        //Scope.Square(guid) + 
        Dot + ((Func<Type[], MethodDefinition>)md.Specialize).Method.Name +
        Scope.Parenthesis(BuildTypeofList(method.GetGenericArguments())) + Dot + typeof(MethodConfig).Name + Semicolon;

      return source;
    }

    public string BuildTypeofList(Type[] types)
    {
      string source = "";
      string comma = "";

      foreach (Type type in types)
      {
        source += comma + "typeof" + Scope.Parenthesis(type.Name);
        comma = Comma + Space;
      }

      return source;
    }

    public virtual string BuildTemplate(params Type[] types)
    {
      if (types.Length == 0)
      {
        return "";
      }

      return Scope.Angle(BuildParams(types, null));
    }

    public virtual bool IsVoid(Type type)
    {
      return type == typeof(void);
    }

    public virtual string BuildParams(bool includeTypes, bool includeNames, params ParameterInfo[] parameters)
    {
      return BuildParams(true, includeTypes, includeNames, parameters);
    }

    public virtual string BuildParams(bool includeRefs, bool includeTypes, bool includeNames, params ParameterInfo[] parameters)
    {
      var types = new List<Type>();
      var names = new List<string>();
      var byref = new List<bool?>();

      foreach (var parm in parameters)
      {
        byref.Add(
          includeRefs && parm.ParameterType.IsByRef ? !parm.IsOut : (bool?)null);

        types.Add(parm.ParameterType);
        names.Add(parm.Name);
      }

      return BuildParams(byref.ToArray(), includeTypes ? types.ToArray() : null, includeNames ? names.ToArray() : null);
    }

    public virtual string BuildParams(Type[] types, string[] names)
    {
      return BuildParams(null, types, names);
    }

    public virtual string BuildParams(bool?[] refOutMods, Type[] types, string[] names)
    {
      int len = types != null ? types.Length : names != null ? names.Length : 0;

      string paramString = "";
      bool first = true;

      for (int i = 0; i < len; i++)
      {
        if (!first)
        {
          paramString += Comma + Space;
        }
        first = false;

        if (refOutMods != null)
        {
          paramString += refOutMods[i].HasValue ?
              (refOutMods[i].Value ? Ref : Out) + Space : "";
        }

        if (types != null)
        {
          paramString += BuildTypeName(types[i]);
        }

        if (types != null && names != null)
        {
          paramString += Space;
        }

        if (names != null)
        {
          paramString += names[i];
        }
      }

      return paramString;
    }

    public virtual string PlainMethodName(MethodInfo method)
    {
      string name = "";

      name = method.Name;

      if (name.StartsWith(GetPrefix) || name.StartsWith(SetPrefix))
      {
        name = name.Substring(4);
      }

      return name;
    }

    public virtual string BuildIfReal(MethodInfo method, string action)
    {
      string ifSource = "";

      ifSource += If + Space;

      ifSource += Scope.Parenthesis(LocalMethodConfig + Dot + IsReal);

      ifSource += Scope.Brackets(action);

      return ifSource;
    }

    public virtual string BuildCount()
    {
      string source = "";

      source += LocalMethodConfig + Dot + Count + Increment + Semicolon;

      return source;
    }

    /// <summary>
    /// Used for both methods and properties. Also, this function checks if the base method is abstract
    /// </summary>
    /// <param name="isProp">method a property</param>
    /// <param name="method">method to build invocation for</param>
    /// <returns></returns>
    public virtual string BuildRealInvoke(bool isProp, MethodInfo method)
    {
      if (method.IsAbstract) //abstract
      {
        return BuildAbstractException(method);
      }
      else
      {
        if (isProp)
        {
          return BuildRealProp(method);
        }
        else //is method
        {
          return BuildRealMethod(method);
        }
      }
    }

    /// <summary>
    /// Build text to throw an exception that the base method cannot be called because it is abstract.
    /// </summary>
    /// <param name="method">Abstract method</param>
    /// <returns></returns>
    public virtual string BuildAbstractException(MethodInfo method)
    {
      string source = "";
      source += Throw + Space + New + Space + typeof(NotImplementedException).FullName;

      source += Scope.Parenthesis(
                  Scope.Quote(
                      string.Format(AbtractMessageFormat, method.Name, ProxyType.Name)));

      source += Semicolon;

      return source;
    }

    /// <summary>
    /// Build text base.Method(...)
    /// </summary>
    /// <param name="method">Method to invoke from base</param>
    /// <returns></returns>
    public virtual string BuildRealMethod(MethodInfo method)
    {
      bool voidReturn = IsVoid(method.ReturnType);

      var ret = new Scope(Scope.WrapTypeEnum.Empty);

      ret.Add(voidReturn ? "" : Return);

      string call = Scope.Parenthesis(BuildParams(false, true, method.GetParameters()));

      ret.Add(Space + Base + Dot + BuildMethodName(method) + call + Semicolon);
      ret.Add(voidReturn ? NewLine + Return + Semicolon : "");

      return ret.ToString();
    }

    /// <summary>
    /// Build text base.Property
    /// </summary>
    /// <param name="method">Property to invoke from base</param>
    /// <returns></returns>
    public virtual string BuildRealProp(MethodInfo method)
    {
      string source;
      string name = PlainMethodName(method);
      bool set = IsVoid(method.ReturnType);

      if (set)
      {
        source = Base + Dot + name + Space + Equal + Space + Value + Semicolon +
          NewLine + Return;
      }
      else
      {
        source = Return + Space + Base + Dot + name;
      }

      source += Semicolon;

      return source;
    }

    public virtual string BuildMethodName(MethodInfo method)
    {
      return PlainMethodName(method) + BuildTemplate(method.GetGenericArguments());
    }

    public virtual string BuildAccess(MethodInfo method)
    {
      return BuildAccess(method, null);
    }

    public virtual string BuildAccess(MethodInfo method, MethodInfo parent)
    {
      if (parent != null && method.IsPublic == parent.IsPublic)
      {
        return "";
      }

      return (method.IsPublic ? Public : Protected) + Space;
    }

    public virtual bool Valid(MethodInfo method)
    {
      return Valid(method, false);
    }

    public virtual bool IsVirtual(MethodInfo method)
    {
      if (IsInternal(method))
      {
        return false;
      }

      return !method.IsFinal && method.IsVirtual;
    }

    public virtual bool Valid(MethodInfo method, bool includeProperties)
    {
      if (!IsVirtual(method))
      {
        return false;
      }

      var badMethods = new List<string>()
            {
                "Finalize",
                "MemberwiseClone",
            };

      string name = PlainMethodName(method);

      if (badMethods.Contains(name))
      {
        return false;
      }

      //don't override events
      var badPrefixes = new List<string>() 
            {
                "add_",
                "remove_",
            };

      foreach (var badPrefix in badPrefixes)
      {
        if (name.StartsWith(badPrefix))
        {
          return false;
        }
      }

      if (!includeProperties)
      {
        if (method.Name.StartsWith(GetPrefix) || method.Name.StartsWith(SetPrefix))
        {
          return false;
        }
      }

      return true;
    }

    public virtual string BuildMethod(MethodInfo method)
    {
      string source = "";

      source += BuildMethodSignature(method);

      var body = new Scope(Scope.WrapTypeEnum.Brackets);

      body.Add(GetProxyMethod(method));
      body.Add(BuildCount());

      body.Add(BuildIfReal(method, BuildRealInvoke(false, method)));

      body.Add(BuildFakeInvoke(method));

      source += body.ToString();

      return source;
    }

    public virtual string BuildFakeInvoke(MethodInfo method)
    {
      return BuildFakeInvoke(method, null);
    }
    public virtual string BuildFakeInvoke(MethodInfo method, string raiseOverride)
    {
      var parms = method.GetParameters();
      var body = new Scope(Scope.WrapTypeEnum.Empty);
      body.Add(PrepOuts(parms));

      Type returnType = method.ReturnType;
      string retVal = "retVal";

      string raiseMethod = string.IsNullOrEmpty(raiseOverride) ?
        LocalMethodConfig + Dot + Intercept : raiseOverride;

      string raiseRef = string.IsNullOrEmpty(raiseOverride) ?
        "" : "raiseRef" + Space + Comma;

      string returnCast = IsVoid(returnType) ? "" :
        (Return + Space + Scope.Parenthesis(BuildTypeName(returnType)) + retVal + Semicolon);

      body.Add(Var + Space + retVal + Equal + raiseMethod + 
        Scope.Parenthesis(raiseRef + LocalWrapperVariable) + Semicolon);

      body.Add(AssignOuts(parms));
      body.Add(returnCast);

      string result = body.ToString();
      return result;
    }

    public virtual string BuildMethodSignature(MethodInfo method)
    {
      return BuildMethodSignature(method, BuildOverride(method), BuildMethodName(method));
    }
    public virtual string BuildMethodSignature(MethodInfo method, string overrideTxt, string methodName)
    {
      string source = "";
      source += BuildAccess(method) + Space + overrideTxt + Space;
      source += IsVoid(method.ReturnType) ? VoidName : BuildTypeName(method.ReturnType);
      source += Space;
      source += methodName;

      source += Scope.Parenthesis(BuildParams(true, true, method.GetParameters()));

      return source;
    }

    public virtual string PrepOuts(ParameterInfo[] parameters)
    {
      string source = "";

      string parms = "";
      string comma = "";

      foreach (var pi in parameters)
      {
        string name;

        if (pi.IsOut)
        {
          Type type = pi.ParameterType.GetElementType();

          if (type.IsValueType)
          {
            name = string.Format("default({0})", BuildTypeName(type));
          }
          else
          {
            name = Null;
          }
        }
        else
        {
          name = pi.Name;
        }

        parms += comma + name;
        comma = Comma + Space;
      }

      source += Var + Space + LocalWrapperVariable + Equal + New + Space +
        BuildTypeName(typeof(object)) +
        Scope.Square() + Space +
        Scope.Brackets(parms) +
        Semicolon;

      return source;
    }
    public virtual string AssignOuts(ParameterInfo[] parameters)
    {
      string source = "";

      foreach (var pi in EachByRef(parameters, true, true))
      {
        source += pi.Name + Equal +
          Scope.Parenthesis(BuildTypeName(pi.ParameterType)) +
          LocalWrapperVariable + Scope.Square(pi.Position.ToString()) + Semicolon + NewLine;
      }

      return source;
    }
    public virtual IEnumerable<ParameterInfo> EachByRef(ParameterInfo[] parameters, bool refs, bool outs)
    {
      foreach (var pi in parameters)
      {
        if (!pi.ParameterType.IsByRef)
        {
          continue;
        }

        if ((refs && outs) || (refs && !pi.IsOut) || (outs && pi.IsOut))
        {
          yield return pi;
        }
      }
    }

    public virtual string PrepWrapParamsArray(ParameterInfo[] parameters)
    {
      string source = "";

      if (parameters.Length != 1 || !parameters[0].ParameterType.IsArray)
      {
        return source;
      }

      var paramInfo = parameters[0];

      source += Var + Space + LocalWrapperVariable + Space + Equal + Space;
      source += New + Space + "object" + Scope.Square("1") + Semicolon + NewLine;

      source += LocalWrapperVariable + Scope.Square("0") + Space + Equal + Space;
      source += paramInfo.Name + Semicolon + NewLine;

      return source;
    }

    public virtual string UseParamsArrayWrapper(string paramListText, params ParameterInfo[] parameters)
    {
      if (parameters.Length != 1 || !parameters[0].ParameterType.IsArray)
      {
        return paramListText.Replace(Ref + Space, "").Replace(Out + Space, "");
      }

      return LocalWrapperVariable;
    }

    public class Scope
    {
      public enum WrapTypeEnum
      {
        Brackets = 0,
        Parenthesis = 1,
        Angle = 2,
        Empty = 3,
        Quotes = 4,
        Square = 5
      };

      protected WrapTypeEnum WrapType { get; set; }

      protected string[] WrapSymbols { get; set; }

      protected string Text { get; set; }

      public Scope(WrapTypeEnum type)
      {
        Text = "";

        WrapSymbols = new string[]
                {
                    "{}",
                    "()",
                    "<>",
                    "",
                    "\"\"",
                    "[]"
                };

        WrapType = type;
      }

      public void Add(string text)
      {
        Text = Text + GetCurrentNewLine() + text;
      }

      public void Clear()
      {
        Text = "";
      }

      public virtual string GetCurrentNewLine()
      {
        if (string.IsNullOrEmpty(Text))
        {
          return "";
        }

        switch (WrapType)
        {
          case Scope.WrapTypeEnum.Parenthesis:
            return ", ";
          case Scope.WrapTypeEnum.Angle:
          case Scope.WrapTypeEnum.Empty:
          case Scope.WrapTypeEnum.Quotes:
          case Scope.WrapTypeEnum.Square:
            return "";
          //case Scope.WrapTypeEnum.Brackets:
          default:
            return "\r\n";
        }
      }

      public static string Angle(params string[] text)
      {
        return Wrap(WrapTypeEnum.Angle, text);
      }
      public static string Parenthesis(params string[] text)
      {
        return Wrap(WrapTypeEnum.Parenthesis, text);
      }
      public static string Quote(params string[] text)
      {
        return Wrap(WrapTypeEnum.Quotes, text);
      }
      public static string Square(params string[] text)
      {
        return Wrap(WrapTypeEnum.Square, text);
      }
      public static string Brackets(params string[] text)
      {
        return Wrap(WrapTypeEnum.Brackets, text);
      }
      public static string Wrap(WrapTypeEnum type, params string[] strings)
      {
        var scope = new Scope(type);

        foreach (string text in strings)
        {
          scope.Add(text);
        }

        return scope.ToString();
      }

      public override string ToString()
      {
        int wrapTypeIndex = (int)WrapType;
        string open = WrapType == WrapTypeEnum.Empty ? "" : WrapSymbols[wrapTypeIndex][0].ToString();
        string close = WrapType == WrapTypeEnum.Empty ? "" : WrapSymbols[wrapTypeIndex][1].ToString();

        return open + Text + close;
      }
    }

    public virtual string BuildOverride(PropertyInfo pi)
    {
      return BuildOverride(pi.GetAccessors(true)[0]);
    }

    public virtual string BuildOverride(MethodInfo mi)
    {
      if (!mi.DeclaringType.IsInterface)
      {
        return Override;
      }

      return Virtual;
    }

    public virtual void ConfigureMethods(IProxy iproxy)
    {
      iproxy.Builder = this;
      iproxy.DefaultBehavior = CtorConfig.DefaultBehavior;
      iproxy.ExpectingPropertyUse = false;
      iproxy.MethodGroup = new BeanboxSoftware.BeanProxy.Recording.MethodGroup();
      
      //Delegate.CreateDelegate has issues with interface methods that use delegate type parameters or returns
      EnsureMethodInfoEntryPoints(MethodDefinitions, iproxy);
      
      iproxy.MethodDefinitions = new Dictionary<Guid, MethodDefinition>();

      foreach (var md in MethodDefinitions)
      {
        md.BindTo(iproxy);
        iproxy.MethodDefinitions[md.Id] = md;
      }

      iproxy.MethodConfigurationsReady = true;
    }

    public virtual string BuildThrow(string localVariableName, string msg)
    {
      return "throw new " + BuildTypeName(typeof(BuilderExceptionHelper)) + Scope.Parenthesis(localVariableName, Scope.Quote(msg)) + Semicolon;
    }

    /// <summary>
    /// Delegate.CreateDelegate has issues with interface methods that use delegate type parameters or returns
    /// If the Proxy type is an interface, this method will remap all method infos to the runtime proxy type, 
    /// rather than the interface type provided
    /// </summary>
    /// <param name="iprox">iproxy object</param>
    private void EnsureMethodInfoEntryPoints(List<MethodDefinition> defs, IProxy iprox)
    {
      if (!ProxyType.IsInterface)
      {
        return;
      }

      foreach (var mi in AllMethods(iprox.GetType(), true))
      {
        var md = defs.Find(mi);

        //some methods may not be exposed that the base had, such as ToString [perhaps I should change that]
        if (md == null)
          continue;

        md.MethodInfo = mi; //corrected!
      }
    }
  }

  public class BuilderExceptionHelper : Exception
  {
    public object ObjectData { get; set; }
    public BuilderExceptionHelper(object obj, string msg) : base(msg) { ObjectData = obj; }
  }
}
